// --------------------------------------------------------------------------------------------------------------------
// <copyright file="BingMapsService.cs" company="XamlNinja">
//   2011 Richard Griffin and Ollie Riches
// </copyright>
// <summary>
//   BingMapsService.cs
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace WP7Contrib.Services.BingMaps
{
    using System;
    using System.Collections.Generic;
    using System.Device.Location;
    using System.Globalization;
    using System.Linq;
    using System.Reactive.Linq;
    using System.Text;
    using System.Windows;
    using Caching;
    using Collections;
    using Common.Extensions;
    using Common.Helpers;
    using Communications;
    using Logging;
    using Microsoft.Phone.Controls.Maps;
    using Model;
    using Resources.Routes;
    using Address = Model.Address;
    using Confidence = Model.Confidence;
    using ItineraryItem = Model.ItineraryItem;
    using Point = Resources.Point;
    using RouteLeg = Model.RouteLeg;

    /// <summary>
    /// Wrapper class for the Bing Maps REST API. Abstracts the user away from REST and HTTP concerns and exposes results via Rx methods.
    /// </summary>
    public sealed class BingMapsService : IBingMapsService
    {
        private const string FailedLocationSearch = "BingMapsService: Failed to call bing location rest api, message - '{0}'";

        private const string FailedRouteSearch = "BingMapsService: Failed to call bing route rest api, message - '{0}'";

        private const string FailedServiceSearch = "BingMapsService: Failed to call bing service search rest api, message - '{0}'";

        private const string FailedImageryUrl = "BingMapsService: Failed to build bing imagery url, message - '{0}'";

        private readonly IHandleResourcesFactory resourceHandlerFactory;

        private readonly IEncodeProperties propertyEncoder;

        private readonly ICacheProvider cacheProvider;

        private readonly ISettings settings;

        private readonly TimeSpan cacheTimeout;

        private readonly TimeSpan timeout;

        private readonly int retry;

        private readonly ILog log;

        private readonly string Spacer;

        public BingMapsService(string credentialsId, string appId)
            : this(new ResourceClientFactory(new DebugLog()), new UrlEncoder(), new InMemoryCacheProvider(), new Settings(credentialsId, appId), new DebugLog())
        {
        }

        public BingMapsService(string credentialsId, string appId, ILog log)
            : this(new ResourceClientFactory(log), new UrlEncoder(), new InMemoryCacheProvider(), new Settings(credentialsId, appId), log)
        {
        }

        public BingMapsService(IHandleResourcesFactory resourceHandlerFactory, IEncodeProperties propertyEncoder, ISettings settings)
            : this(resourceHandlerFactory, propertyEncoder, new InMemoryCacheProvider(), settings, new DebugLog())
        {
        }

        public BingMapsService(IHandleResourcesFactory resourceHandlerFactory,
                              IEncodeProperties propertyEncoder,
                              ICacheProvider cacheProvider,
                              ISettings settings,
                              ILog log)
        {
            this.resourceHandlerFactory = resourceHandlerFactory;
            this.propertyEncoder = propertyEncoder;
            this.cacheProvider = cacheProvider;
            this.settings = settings;
            this.log = log;

            this.cacheTimeout = TimeSpan.FromMilliseconds(this.settings.CacheTimeout);
            this.timeout = TimeSpan.FromMilliseconds(this.settings.Timeout);
            this.retry = this.settings.Retry;

            this.Spacer = this.propertyEncoder.Encode(" ");
        }

        #region Implementation of IBingMapsService

        public IObservable<LocationSearchResult> SearchForLocationUsingAddress(ILocationSearchAddressCriterion criterion)
        {
            this.log.Write("BingMapsService: SearchForLocationUsingAddress...");

            try
            {
                var keyTuple = new CacheTuple<string, ILocationSearchAddressCriterion>
                {
                    Val1 = "SearchForLocationUsingAddress",
                    Val2 = criterion.DeepClone()
                };

                var locationResult = this.cacheProvider.Get<CacheTuple<string, ILocationSearchAddressCriterion>, LocationSearchResult>(keyTuple);
                if (locationResult != null)
                {
                    this.log.Write("BingMapsService: SearchForLocationUsingAddress results retrieved from cache, hash code - {0}", criterion.GetHashCode());
                    return Observable.Return(locationResult.DeepClone()).AsObservable();
                }

                var resourceHandler = this.resourceHandlerFactory.Create()
                   .ForType(ResourceType.Json)
                   .WithGzip()
                   .UseUrlForGet(this.settings.SearchLocationUsingAddressUrl);

                var @params = new[]
                    {
                        // path parameters...

                        // query string parameters...
                        this.propertyEncoder.Encode(criterion.Address.CountryRegion),
                        this.propertyEncoder.Encode(criterion.Address.AdminDistrict),
                        this.propertyEncoder.Encode(criterion.Address.Locality),
                        this.propertyEncoder.Encode(criterion.Address.PostalCode),
                        this.propertyEncoder.Encode(criterion.Address.AddressLine),
                        this.settings.CredentialsId
                };

                return resourceHandler.Get<Resources.Location.Result>(@params)
                    .Timeout(this.timeout)
                    .Retry(this.retry)
                    .Finally(() =>
                    {
                        GC.Collect();
                        GC.WaitForPendingFinalizers();
                    })
                    .Select(response =>
                    {
                        var result = this.ProcessResponse(response);
                        if (result.StatusCode == "200")
                        {
                            this.cacheProvider.Add(keyTuple, result.DeepClone(), this.cacheTimeout);
                        }
                        return result;
                    });
    }
            catch (Exception exn)
            {
                var message = string.Format(FailedLocationSearch, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IObservable<LocationSearchResult> SearchForLocationUsingPoint(ILocationSearchPointCriterion criterion)
        {
            this.log.Write("BingMapsService: SearchForLocationUsingPoint...");

            try
            {
                var keyTuple = new CacheTuple<string, ILocationSearchPointCriterion>
                {
                    Val1 = "SearchForLocationUsingPoint",
                    Val2 = criterion.DeepClone()
                };

                var locationResult = this.cacheProvider.Get<CacheTuple<string, ILocationSearchPointCriterion>, LocationSearchResult>(keyTuple);
                if (locationResult != null)
                {
                    this.log.Write("BingMapsService: SearchForLocationUsingPoint results retrieved from cache, hash code - {0}", criterion.GetHashCode());
                    return Observable.Return(locationResult.DeepClone()).AsObservable();
                }

                var resourceHandler = this.resourceHandlerFactory.Create()
                   .ForType(ResourceType.Json)
                   .WithGzip()
                   .UseUrlForGet(this.settings.SearchLocationUsingPointUrl);

                var @params = new[]
                    {
                        // path parameters...
                        string.Format(CultureInfo.InvariantCulture, "{0},{1}", criterion.Point.Latitude, criterion.Point.Longitude),

                        // query string parameters...
                        BuildQueryString(criterion.IncludedEntities),
                        this.settings.CredentialsId
                };

                return resourceHandler.Get<Resources.Location.Result>(@params)
                    .Timeout(this.timeout)
                    .Retry(this.retry)
                    .Finally(() =>
                    {
                        GC.Collect();
                        GC.WaitForPendingFinalizers();
                    })
                    .Select(response =>
                    {
                        var result = this.ProcessResponse(response);
                        if (result.StatusCode == "200")
                        {
                            this.cacheProvider.Add(keyTuple, result.DeepClone(), this.cacheTimeout);
                        }
                        return result;
                    });
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedLocationSearch, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IObservable<LocationSearchResult> FindLocationUsingQuery(ILocationSearchQueryCriterion criterion)
        {
            this.log.Write("BingMapsService: FindLocationUsingQuery...");

            try
            {
                var keyTuple = new CacheTuple<string, ILocationSearchQueryCriterion>
                {
                    Val1 = "FindLocationUsingQuery",
                    Val2 = criterion.DeepClone()
                };

                var locationResult = this.cacheProvider.Get<CacheTuple<string, ILocationSearchQueryCriterion>, LocationSearchResult>(keyTuple);
                if (locationResult != null)
                {
                    this.log.Write("BingMapsService: FindLocationUsingQuery results retrieved from cache, hash code - {0}", criterion.GetHashCode());
                    return Observable.Return(locationResult.DeepClone()).AsObservable();
                }

                var resourceHandler = this.resourceHandlerFactory.Create()
                  .ForType(ResourceType.Json)
                  .WithGzip()
                  .UseUrlForGet(this.settings.FindLocationUsingQueryUrl);

                var @params = new[]
                {
                    // path parameters...

                    // query string parameters...
                    "query=" + this.propertyEncoder.Encode(criterion.Query),
                    this.propertyEncoder.Encode(this.settings.CredentialsId)
                };

                return resourceHandler.Get<Resources.Location.Result>(@params)
                    .Timeout(this.timeout)
                    .Retry(this.retry)
                    .Finally(() =>
                    {
                        GC.Collect();
                        GC.WaitForPendingFinalizers();
                    })
                    .Select(response =>
                    {
                        var result = this.ProcessResponse(response);
                        if (result.StatusCode == "200")
                        {
                            this.cacheProvider.Add(keyTuple, result.DeepClone(), this.cacheTimeout);
                        }
                        return result;
                    });
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedLocationSearch, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public Uri ImageryUrlForCenterPoint(IImageryUrlForCenterPointCriterion criterion)
        {
            this.log.Write("BingMapsService: ImageryUrlForCenterPoint...");

            try
            {
                var centrePoint = string.Format(CultureInfo.InvariantCulture, "{0},{1}", criterion.CenterPoint.Latitude, criterion.CenterPoint.Longitude);

                var mapSize = string.Empty;
                if (criterion.MapSize.HasValue && criterion.MapSize != Size.Empty)
                    mapSize = string.Format("{0},{1}", criterion.MapSize.Value.Width, criterion.MapSize.Value.Height);

                var pushPins = string.Empty;
                if (criterion.PushPins != null && criterion.PushPins.Count != 0)
                    pushPins = this.BuildPushPins(criterion.PushPins);

                var @params = new[]
                    {
                        // path parameters...
                        criterion.ImagerySet.ToString(),
                        centrePoint,
                        criterion.ZoomLevel.ToString(),

                        // query string parameters...
                        this.propertyEncoder.Encode(mapSize),
                        this.propertyEncoder.Encode(criterion.MapLayer),
                        this.propertyEncoder.Encode(criterion.MapVersion),
                        pushPins,
                        this.settings.CredentialsId
                    };

                var url = new Uri(string.Format(this.settings.ImageryCenterPointUrl, @params));
                this.log.Write("BingMapsService: Url - " + url.AbsoluteUri);

                return url;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedImageryUrl, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public Uri ImageryUrlForCenterPointWithRoute(IImageryUrlForCenterPointWithRouteCriterion criterion)
        {
            this.log.Write("BingMapsService: ImageryUrlForCenterPointWithRoute...");

            try
            {
                var centrePoint = string.Format(CultureInfo.InvariantCulture, "{0},{1}", criterion.CenterPoint.Latitude, criterion.CenterPoint.Longitude);

                var mapSize = string.Empty;
                if (criterion.MapSize.HasValue && criterion.MapSize != Size.Empty)
                    mapSize = string.Format("{0},{1}", criterion.MapSize.Value.Width, criterion.MapSize.Value.Height);

                var i = 0;
                var waypoints = new StringBuilder();
                foreach (var waypoint in criterion.WayPoints)
                {
                    waypoints.Append(this.BuildQueryString(waypoint, (++i).ToString()));
                }

                var pushPins = string.Empty;
                if (criterion.PushPins != null && criterion.PushPins.Count != 0)
                    pushPins = this.BuildPushPins(criterion.PushPins);

                var dateTime = string.Empty;
                if (criterion.DateTime.HasValue)
                    dateTime = criterion.DateTime.Value.ToString("MM/dd/yyyy hh:mm:ss");

                var @params = new[]
                    {
                        // path parameters...
                        criterion.ImagerySet.ToString(),
                        centrePoint,
                        criterion.ZoomLevel.ToString(),
                        criterion.TravelMode.ToString(),

                        // query string parameters...
                        waypoints.ToString().Remove(0, 1),
                        this.propertyEncoder.Encode(mapSize),
                        BuildQueryString(criterion.Avoid),
                        pushPins,
                        this.propertyEncoder.Encode(criterion.TimeType),
                        this.propertyEncoder.Encode(dateTime),
                        this.propertyEncoder.Encode(criterion.MaxSolutions),
                        this.propertyEncoder.Encode(criterion.MapLayer),
                        this.propertyEncoder.Encode(criterion.MapVersion),
                        this.settings.CredentialsId
                };

                var url = new Uri(string.Format(this.settings.ImageryCenterPointWithRouteUrl, @params));
                this.log.Write("BingMapsService: Url - " + url.AbsoluteUri);

                return url;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedImageryUrl, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public Uri ImageryUrlForSpecificArea(IImageryUrForSpecificAreaCriterion criterion)
        {
            this.log.Write("BingMapsService: ImageryUrlForSpecificArea...");

            try
            {
                var mapArea = string.Empty;
                if (criterion.MapArea != null)
                    mapArea = string.Format("{0},{1},{2},{3}", criterion.MapArea.South, criterion.MapArea.West, criterion.MapArea.North, criterion.MapArea.East);

                var mapSize = string.Empty;
                if (criterion.MapSize.HasValue && criterion.MapSize != Size.Empty)
                    mapSize = string.Format("{0},{1}", criterion.MapSize.Value.Width, criterion.MapSize.Value.Height);

                var pushPins = string.Empty;
                if (criterion.PushPins != null && criterion.PushPins.Count != 0)
                    pushPins = BuildPushPins(criterion.PushPins);

                var @params = new[]
                    {
                        // path parameters...
                        criterion.ImagerySet.ToString(),

                        // query string parameters...
                        this.propertyEncoder.Encode(mapArea),
                        this.propertyEncoder.Encode(mapSize),
                        pushPins,
                        this.propertyEncoder.Encode(criterion.MapLayer),
                        this.propertyEncoder.Encode(criterion.MapVersion),
                        this.settings.CredentialsId
                    };

                var url = new Uri(string.Format(this.settings.ImageryMapAreaUrl, @params));
                this.log.Write("BingMapsService: Url - " + url.AbsoluteUri);

                return url;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedImageryUrl, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public Uri ImageryUrlForSpecificAreaWithRoute(IImageryUrForSpecificAreaWithRouteCriterion criterion)
        {
            this.log.Write("BingMapsService: ImageryUrlForSpecificAreaWithRoute...");

            try
            {
                var i = 0;
                var waypoints = new StringBuilder();
                foreach (var waypoint in criterion.WayPoints)
                {
                    waypoints.Append(this.BuildQueryString(waypoint, (++i).ToString()));
                }

                var mapArea = string.Empty;
                if (criterion.MapArea != null)
                    mapArea = string.Format("{0},{1},{2},{3}", criterion.MapArea.South, criterion.MapArea.West, criterion.MapArea.North, criterion.MapArea.East);

                var pushPins = string.Empty;
                if (criterion.PushPins != null && criterion.PushPins.Count != 0)
                    pushPins = this.BuildPushPins(criterion.PushPins);

                var dateTime = string.Empty;
                if (criterion.DateTime.HasValue)
                    dateTime = criterion.DateTime.Value.ToString("MM/dd/yyyy hh:mm:ss");

                var @params = new[]
                    {
                        // path parameters...
                        this.propertyEncoder.Encode(criterion.ImagerySet),
                        this.propertyEncoder.Encode(criterion.TravelMode),

                        // query string parameters...
                        waypoints.ToString().Remove(0, 1),
                        this.propertyEncoder.Encode(mapArea),
                        BuildQueryString(criterion.Avoid),
                        pushPins,
                        this.propertyEncoder.Encode(criterion.TimeType),
                        this.propertyEncoder.Encode(dateTime),
                        this.propertyEncoder.Encode(criterion.MaxSolutions),
                        this.propertyEncoder.Encode(criterion.MapLayer),
                        this.propertyEncoder.Encode(criterion.MapVersion),
                        this.settings.CredentialsId
                    };

                var url = new Uri(string.Format(this.settings.ImageryMapAreaWithRouteUrl, @params));
                this.log.Write("BingMapsService: Url - " + url.AbsoluteUri);

                return url;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedImageryUrl, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public Uri ImageryUrlForRoute(IImageryUrlForRouteCriterion criterion)
        {
            this.log.Write("BingMapsService: ImageryUrlForRoute...");

            try
            {
                var i = 0;
                var waypoints = new StringBuilder();
                foreach (var waypoint in criterion.WayPoints)
                {
                    waypoints.Append(this.BuildQueryString(waypoint, (++i).ToString()));
                }

                var mapSize = string.Empty;
                if (criterion.MapSize.HasValue && criterion.MapSize != Size.Empty)
                    mapSize = string.Format("{0},{1}", criterion.MapSize.Value.Width, criterion.MapSize.Value.Height);

                var pushPins = string.Empty;
                if (criterion.PushPins != null && criterion.PushPins.Count != 0)
                    pushPins = this.BuildPushPins(criterion.PushPins);

                var dateTime = string.Empty;
                if (criterion.DateTime.HasValue)
                    dateTime = criterion.DateTime.Value.ToString("MM/dd/yyyy hh:mm:ss");
                
                var @params = new[]
                    {
                        // path parameters...
                        criterion.ImagerySet.ToString(),
                        criterion.TravelMode.ToString(),

                        // query string parameters...
                        this.propertyEncoder.Encode(BuildQueryString(criterion.Avoid)),
                        this.propertyEncoder.Encode(dateTime),
                        this.propertyEncoder.Encode(criterion.MapLayer),
                        this.propertyEncoder.Encode(mapSize),
                        this.propertyEncoder.Encode(criterion.MaxSolutions),
                        this.propertyEncoder.Encode(criterion.MapVersion),
                        this.propertyEncoder.Encode(criterion.Optimize),
                        pushPins,
                        this.propertyEncoder.Encode(criterion.TimeType),
                        waypoints.ToString(),
                        this.settings.CredentialsId
                    };

                var url = new Uri(string.Format(this.settings.ImageryMapRouteUrl, @params));
                this.log.Write("BingMapsService: Url - " + url.AbsoluteUri);

                return url;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedImageryUrl, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public Uri ImageryUrlForQuery(IImageryUrlForQueryCriterion criterion)
        {
            this.log.Write("BingMapsService: ImageryUrlForQuery...");

            try
            {
                var mapSize = string.Empty;
                if (criterion.MapSize.HasValue && criterion.MapSize != Size.Empty)
                    mapSize = string.Format("{0},{1}", criterion.MapSize.Value.Width, criterion.MapSize.Value.Height);

                var @params = new[]
                    {
                        // path parameters...
                        criterion.ImagerySet.ToString(),
                        this.propertyEncoder.Encode(criterion.Query),

                        // query string parameters...
                        this.propertyEncoder.Encode(mapSize),
                        this.propertyEncoder.Encode(criterion.MapLayer),
                        this.propertyEncoder.Encode(criterion.MapVersion),
                        this.settings.CredentialsId
                };

                var url = new Uri(string.Format(this.settings.ImageryQueryUrl, @params));
                this.log.Write("BingMapsService: Url - " + url.AbsoluteUri);

                return url;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedImageryUrl, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IObservable<ImageryMetadataSearchResult> GetImageryMetadataForAnImagerySet(IImageryMetadataForAnImagerySetCriterion criterion)
        {
            this.log.Write("BingMapsService: GetImageryMetadataForAnImagerySet...");

            try
            {
                var keyTuple = new CacheTuple<string, IImageryMetadataForAnImagerySetCriterion>
                {
                    Val1 = "GetImageryMetadataForAnImagerySet",
                    Val2 = criterion
                };

                var searchResult = this.cacheProvider.Get<CacheTuple<string, IImageryMetadataForAnImagerySetCriterion>, ImageryMetadataSearchResult>(keyTuple);
                if (searchResult != null)
                {
                    this.log.Write("BingMapsService: GetImageryMetadataForAnImagerySet results retrieved from cache, hash code - {0}", criterion.GetHashCode());
                    return Observable.Return(searchResult.DeepClone()).AsObservable();
                }

                var resourceHandler = this.resourceHandlerFactory.Create()
                  .ForType(ResourceType.Json)
                  .WithGzip()
                  .UseUrlForGet(this.settings.ImageryMetadataUrl);

                var @params = new[]
                {
                    // path parameters...
                    criterion.ImagerySet.ToString(),

                    // query string parameters...
                    this.propertyEncoder.Encode(this.settings.CredentialsId)
                };

                return resourceHandler.Get<Resources.ImageryMetadata.Result>(@params)
                    .Timeout(this.timeout)
                    .Retry(this.retry)
                    .Finally(() =>
                    {
                        GC.Collect();
                        GC.WaitForPendingFinalizers();
                    })
                    .Select(response =>
                    {
                        var result = this.ProcessResponse(response);
                        if (result.StatusCode == "200")
                        {
                            this.cacheProvider.Add(keyTuple, result.DeepClone(), this.cacheTimeout);
                        }
                        return result;
                    });
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedLocationSearch, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IObservable<ImageryMetadataSearchResult> GetImageryMetadataForAnImagerySetAtSpecificLocation(IImageryMetadataForAnImagerySetAtSpecificLocationCriterion criterion)
        {
            this.log.Write("BingMapsService: GetImageryMetadataForAnImagerySetAtSpecificLocation...");

            try
            {
                var keyTuple = new CacheTuple<string, IImageryMetadataForAnImagerySetAtSpecificLocationCriterion>
                {
                    Val1 = "GetImageryMetadataForAnImagerySetAtSpecificLocation",
                    Val2 = criterion
                };

                var searchResult = this.cacheProvider.Get<CacheTuple<string, IImageryMetadataForAnImagerySetAtSpecificLocationCriterion>, ImageryMetadataSearchResult>(keyTuple);
                if (searchResult != null)
                {
                    this.log.Write("BingMapsService: GetImageryMetadataForAnImagerySetAtSpecificLocation results retrieved from cache, hash code - {0}", criterion.GetHashCode());
                    return Observable.Return(searchResult.DeepClone()).AsObservable();
                }

                var resourceHandler = this.resourceHandlerFactory.Create()
                  .ForType(ResourceType.Json)
                  .WithGzip()
                  .UseUrlForGet(this.settings.ImageryMetadataAtSpecificLocationUrl);

                var centrePoint = string.Format(CultureInfo.InvariantCulture, "{0},{1}", criterion.CenterPoint.Latitude, criterion.CenterPoint.Longitude);

                var include = string.Empty;
                if (criterion.Include != null)
                {
                    include = string.Format("&include={0}", criterion.Include);
                }

                var @params = new[]
                {
                    // path parameters...
                    criterion.ImagerySet.ToString(),
                    centrePoint,
                    
                    // query string parameters...
                    this.propertyEncoder.Encode(criterion.Orientation),
                    this.propertyEncoder.Encode(criterion.ZoomLevel),
                    include,
                    this.propertyEncoder.Encode(this.settings.CredentialsId)
                };

                return resourceHandler.Get<Resources.ImageryMetadata.Result>(@params)
                    .Timeout(this.timeout)
                    .Retry(this.retry)
                    .Finally(() =>
                    {
                        GC.Collect();
                        GC.WaitForPendingFinalizers();
                    })
                    .Select(response =>
                    {
                        var result = this.ProcessResponse(response);
                        if (result.StatusCode == "200")
                        {
                            this.cacheProvider.Add(keyTuple, result.DeepClone(), this.cacheTimeout);
                        }
                        return result;
                    });
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedLocationSearch, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IObservable<ImageryMetadataSearchResult> GetImageryMetadataForBasicImagerySetAtSpecificLocation(IImageryMetadataForBasicImagerySetAtSpecificLocationCriterion criterion)
        {
            this.log.Write("BingMapsService: GetImageryMetadataForBasicImagerySetAtSpecificLocation...");

            try
            {
                var keyTuple = new CacheTuple<string, IImageryMetadataForBasicImagerySetAtSpecificLocationCriterion>
                {
                    Val1 = "GetImageryMetadataForBasicImagerySetAtSpecificLocation",
                    Val2 = criterion
                };

                var searchResult = this.cacheProvider.Get<CacheTuple<string, IImageryMetadataForBasicImagerySetAtSpecificLocationCriterion>, ImageryMetadataSearchResult>(keyTuple);
                if (searchResult != null)
                {
                    this.log.Write("BingMapsService: GetImageryMetadataForBasicImagerySetAtSpecificLocation results retrieved from cache, hash code - {0}", criterion.GetHashCode());
                    return Observable.Return(searchResult.DeepClone()).AsObservable();
                }

                var resourceHandler = this.resourceHandlerFactory.Create()
                  .ForType(ResourceType.Json)
                  .WithGzip()
                  .UseUrlForGet(this.settings.ImageryMetadataBasicAtSpecificLocationUrl);

                var centrePoint = string.Format(CultureInfo.InvariantCulture, "{0},{1}", criterion.CenterPoint.Latitude, criterion.CenterPoint.Longitude);

                var include = string.Empty;
                if (criterion.Include != null)
                {
                    include = string.Format("&include={0}", criterion.Include);
                }

                var @params = new[]
                {
                    // path parameters...
                    criterion.ImagerySet.ToString(),
                    centrePoint,
                    
                    // query string parameters...
                    this.propertyEncoder.Encode(criterion.Orientation),
                    this.propertyEncoder.Encode(criterion.ZoomLevel),
                    include,
                    this.propertyEncoder.Encode(this.settings.CredentialsId)
                };

                return resourceHandler.Get<Resources.ImageryMetadata.Result>(@params)
                    .Timeout(this.timeout)
                    .Retry(this.retry)
                    .Finally(() =>
                    {
                        GC.Collect();
                        GC.WaitForPendingFinalizers();
                    })
                    .Select(response =>
                    {
                        var result = this.ProcessResponse(response);
                        if (result.StatusCode == "200")
                        {
                            this.cacheProvider.Add(keyTuple, result.DeepClone(), this.cacheTimeout);
                        }
                        return result;
                    });
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedLocationSearch, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }
        
        public IObservable<RouteSearchResult> CalculateARoute(IRouteSearchCriterion criterion)
        {
            this.log.Write("BingMapsService: CalculateARoute...");

            try
            {
                var keyTuple = new CacheTuple<string, IRouteSearchCriterion>
                                   {
                                       Val1 = "CalculateARoute",
                                       Val2 = criterion.DeepClone()
                                   };

                var routeData = this.cacheProvider.Get<CacheTuple<string, IRouteSearchCriterion>, RouteSearchResult>(keyTuple);
                if (routeData != null)
                {
                    this.log.Write("BingMapsService: CalculateARoute results retrieved from cache, hash code - {0}.", keyTuple.GetHashCode());
                    return Observable.Return(routeData.DeepClone()).AsObservable();
                }

                var resourceHandler = this.resourceHandlerFactory.Create()
                    .ForType(ResourceType.Json)
                    .WithGzip()
                    .UseUrlForGet(this.settings.CalculateRouteUrl);

                var i = 0;
                var waypoints = new StringBuilder();
                foreach (var waypoint in criterion.WayPoints)
                {
                    waypoints.Append(this.BuildQueryString(waypoint, (++i).ToString()));
                }

                var dateTime = string.Empty;
                if (criterion.DateTime.HasValue)
                    dateTime = criterion.DateTime.Value.ToString("MM/dd/yyyy hh:mm:ss");

                var @params = new[]
                    {
                        // path parameters...
                        criterion.TravelMode.ToString(),

                        // query string parameters...
                        waypoints.ToString().Remove(0, 1),
                        this.propertyEncoder.Encode(criterion.Heading),
                        this.propertyEncoder.Encode(criterion.Optimize),
                        BuildQueryString(criterion.Avoid),
                        this.propertyEncoder.Encode(criterion.TimeType),
                        this.propertyEncoder.Encode(dateTime),
                        this.propertyEncoder.Encode(criterion.MaxSolutions),
                        this.propertyEncoder.Encode(criterion.PathOutput),
                        this.propertyEncoder.Encode(criterion.DistanceUnit),
                        this.settings.CredentialsId
                    };

                return resourceHandler.Get<Resources.Routes.Result>(@params)
                    .Timeout(this.timeout)
                    .Retry(this.retry)
                    .Finally(() =>
                    {
                        GC.Collect();
                        GC.WaitForPendingFinalizers();
                    })
                    .Select(response =>
                    {
                        var result = ProcessResponse(response);
                        if (result.StatusCode == "200")
                        {
                            this.cacheProvider.Add(keyTuple, result.DeepClone(), this.cacheTimeout);
                        }
                        return result;
                    });
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedRouteSearch, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IObservable<RouteSearchResult> CalculateRoutesFromMajorRoads(IRouteSearchMajorRoadCriterion criterion)
        {
            this.log.Write("BingMapsService: CalculateRoutesFromMajorRoads...");

            try
            {
                var keyTuple = new CacheTuple<string, IRouteSearchMajorRoadCriterion>
                {
                    Val1 = "CalculateRoutesFromMajorRoads",
                    Val2 = criterion.DeepClone()
                };

                var routedata = this.cacheProvider.Get<CacheTuple<string, IRouteSearchMajorRoadCriterion>, RouteSearchResult>(keyTuple);
                if (routedata != null)
                {
                    this.log.Write("BingMapsService: CalculateRoutesFromMajorRoads results retrieved from cache, hash code - {0}",
                        keyTuple.GetHashCode());
                    return Observable.Return(routedata.DeepClone()).AsObservable();
                }

                var resourceHandler = this.resourceHandlerFactory.Create()
                    .ForType(ResourceType.Json)
                    .WithGzip()
                    .UseUrlForGet(this.settings.CalculateRoutesFromMajorRoadsUrl);

                var @params = new[]
                    {
                        // query string parameters...
                        this.propertyEncoder.Encode(this.BuildQueryString(criterion.Destination)),
                        criterion.Exclude == RouteExclude.Undefined ? string.Empty : propertyEncoder.Encode(criterion.Exclude),
                        this.propertyEncoder.Encode(criterion.PathOutput),
                        this.propertyEncoder.Encode(criterion.DistanceUnit),
                        this.settings.CredentialsId
                    };

                return resourceHandler.Get<Resources.Routes.Result>(@params)
                    .Timeout(this.timeout)
                    .Retry(this.retry)
                    .Finally(() =>
                    {
                        GC.Collect();
                        GC.WaitForPendingFinalizers();
                    })
                    .Select(response =>
                    {
                        var result = this.ProcessResponse(response);
                        if (result.StatusCode == "200")
                        {
                            this.cacheProvider.Add(keyTuple, result.DeepClone(), this.cacheTimeout);
                        }
                        return result;
                    });
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedRouteSearch, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IObservable<SearchResult> SearchForServices(ISearchCriterion criterion)
        {
            this.log.Write("BingMapsService: SearchForServices...");

            try
            {
                var services = this.cacheProvider.Get<ISearchCriterion, SearchResult>(criterion);
                if (services != null)
                {
                    this.log.Write("BingMapsService: SearchForServices results retrieved from cache, hash code - {0}", criterion.GetHashCode());
                    return Observable.Return(services.DeepClone()).AsObservable();
                }

                var resourceHandler = this.resourceHandlerFactory.Create()
                    .ForType(ResourceType.Json)
                    .WithGzip()
                    .UseUrlForGet(this.settings.SearchUrl);

                var @params = new[]
                    {
                            // query string parameters...
                            this.settings.AppId,
                            this.propertyEncoder.Encode(criterion.Culture.Name),
                            criterion.BoundingRectangle.Center.Latitude.ToString(CultureInfo.InvariantCulture),
                            criterion.BoundingRectangle.Center.Longitude.ToString(CultureInfo.InvariantCulture),
                            criterion.Radius.ToString(),
                            this.propertyEncoder.Encode(criterion.Query),
                            criterion.Source.ToString(),
                            criterion.ResultSize.ToString(),
                    };

                return resourceHandler.Get<Resources.Search.Result>(@params)
                    .Timeout(this.timeout)
                    .Retry(this.retry)
                    .Finally(() =>
                    {
                        GC.Collect();
                        GC.WaitForPendingFinalizers();
                    })
                    .Select(response =>
                    {
                        var result = this.ProcessResponse(response);
                        if (result.StatusCode == "200")
                        {
                            this.cacheProvider.Add(criterion.DeepClone(), result.DeepClone(), this.cacheTimeout);
                        }
                        return result;
                    });
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedServiceSearch, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        #endregion

        private string BuildPushPins(IEnumerable<PushPin> pushPins)
        {
            var sb = new StringBuilder();
            foreach (var pushPinString in from pushPin in pushPins
                                          where pushPin != null
                                          select string.Format(CultureInfo.InvariantCulture, "&pushpin={0},{1};{2};{3}",
                                                               pushPin.Point.Latitude,
                                                               pushPin.Point.Longitude,
                                                               pushPin.IconStyle,
                                                               this.propertyEncoder.Encode(pushPin.Label)))
            {
                sb.Append(pushPinString);
            }

            return sb.ToString();
        }

        private LocationSearchResult ProcessResponse(Resources.Location.Result result)
        {
            this.log.Write("BingMapsService: ProcessResponse - LocationSearchResult...");

            if (result == null)
            {
                return null;
            }
            
            var locationResult = new LocationSearchResult
                                     {
                                         AuthenticationResultCode = result.AuthenticationResultCode,
                                         Copyright = result.Copyright,
                                         ErrorDetails = result.ErrorDetails,
                                         StatusCode = result.StatusCode,
                                         StatusDescription = result.StatusDescription,
                                         TraceId = result.TraceId,

                                         Locations = new ObservableCollection<LocationData>()
                                     };

            foreach (var location in result.ResourceSets.SelectMany(resource => resource.Resources))
            {
                locationResult.Locations.Add(new LocationData
                        {
                            Address = new Address
                                    {
                                        AddressLine = location.Address.AddressLine,
                                        AdminDistrict = location.Address.AdminDistrict,
                                        Confidence = MapConfidence(location.Address.Confidence),
                                        CountryRegion = location.Address.CountryRegion,
                                        FormattedAddress = location.Address.FormattedAddress,
                                        Locality = location.Address.Locality,
                                        PostalCode = location.Address.PostalCode
                                    },
                            Confidence = MapConfidence(location.Address.Confidence),
                            BoundingBox = MapBoundingBox(location.BoundingBox),
                            Name = location.Name,
                            Point = MapPoint(location.Point)
                        });
            }

            return locationResult;
        }

        private SearchResult ProcessResponse(Resources.Search.Result result)
        {
            this.log.Write("BingMapsService: ProcessResponse - SearchResult...");

            if (result == null)
            {
                return null;
            }
            
            var searchresult = new SearchResult
                                   {
                                       Query = result.SearchResponse.Query.SearchTerms,
                                       Version = result.SearchResponse.Version
                                   };

            // what sort of search was requested
            // here we are dealing with the phonebook response
            if (result.SearchResponse.PhoneBookResponse != null)
            {
                searchresult.Results = new ObservableCollection<SearchData>();
                foreach (var response in result.SearchResponse.PhoneBookResponse.PhoneBookResult)
                {
                    searchresult.Results.Add(new SearchData
                        {
                            Address = response.Address, 
                            Business = response.Business, 
                            BusinessUrl =
                                !string.IsNullOrEmpty(response.BusinessUrl)
                                    ? new Uri(response.BusinessUrl, UriKind.Absolute)
                                    : null, 
                            City = response.City,
                            CountryOrRegion = response.CountryOrRegion, 
                            DisplayUrl = !string.IsNullOrEmpty(response.DisplayUrl) ? new Uri(response.DisplayUrl, UriKind.Absolute) : null, 
                            Location = new GeoCoordinate(response.Latitude, response.Longitude), 
                            PhoneNumber = response.PhoneNumber,
                            PostalCode = response.PostalCode,
                            ReviewCount = response.ReviewCount,
                            Title = response.Title,
                            UniqueId = response.UniqueId,
                            Url = !string.IsNullOrEmpty(response.Url) ? new Uri(response.Url, UriKind.Absolute) : null,
                            UserRating = response.UserRating,
                        });
                }
            }

            // This search response does not contain any of the result values like other querty results, so
            // we have to manually set them...
            searchresult.StatusCode = "200";
            searchresult.StatusDescription = "OK";

            return searchresult;
        }

        private RouteSearchResult ProcessResponse(Resources.Routes.Result result)
        {
            this.log.Write("BingMapsService: ProcessResponse - RouteSearchResult...");

            if (result == null)
            {
                return null;
            }

            if (!string.IsNullOrEmpty(result.ErrorDetails))
            {
                throw new Exception(string.Format("Failed to process response, error details - '{0}'", result.ErrorDetails));
            }
            
            var routeSearchResult = new RouteSearchResult
                                  {
                                      AuthenticationResultCode = result.AuthenticationResultCode,
                                      Copyright = result.Copyright,
                                      ErrorDetails = result.ErrorDetails,
                                      StatusCode = result.StatusCode,
                                      StatusDescription = result.StatusDescription,
                                      TraceId = result.TraceId
                                  };

            foreach (var resourceSet in result.ResourceSets)
            {
                routeSearchResult.EstimatedTotal = resourceSet.EstimatedTotal;

                foreach (var route in resourceSet.Resources)
                {
                    routeSearchResult.BoundingBox = MapBoundingBox(route.BoundingBox);
                    routeSearchResult.DistanceUnit = route.DistanceUnit;
                    routeSearchResult.DurationUnit = route.DurationUnit;
                    routeSearchResult.TravelDistance = route.TravelDistance;
                    routeSearchResult.TravelDuration = route.TravelDuration;

                    foreach (var routeLeg in route.RouteLegs)
                    {
                        var leg = new RouteLeg();
                        var items = MapItineraryItems(routeLeg.ItineraryItems).ToList();
                        leg.ItineraryItems.AddRange(items);

                        routeSearchResult.RouteLegs.Add(leg);
                    }

                    if (route.RoutePath == null || route.RoutePath.Line == null)
                        continue;

                    route.RoutePath.Line.Coordinates
                        .ForEach(c => routeSearchResult.Points.Add(new GeoCoordinate
                                                                 {
                                                                     Latitude = c[0].GetValueOrDefault(),
                                                                     Longitude = c[1].GetValueOrDefault()
                                                                 }));
                }
            }

            return routeSearchResult;
        }

        private ImageryMetadataSearchResult ProcessResponse(Resources.ImageryMetadata.Result result)
        {
            this.log.Write("BingMapsService: ProcessResponse - ImageryMetadataResult...");

            if (result == null)
            {
                return null;
            }

            var imageryMetadataSearchResult = new ImageryMetadataSearchResult
                {
                    AuthenticationResultCode = result.AuthenticationResultCode,
                    Copyright = result.Copyright,
                    ErrorDetails = result.ErrorDetails,
                    StatusCode = result.StatusCode,
                    StatusDescription = result.StatusDescription,
                    TraceId = result.TraceId
                };

            foreach (var resourceSet in result.ResourceSets)
            {
                imageryMetadataSearchResult.EstimatedTotal = resourceSet.EstimatedTotal;

                foreach (var resource in resourceSet.Resources)
                {
                    var metadata = new Metadata
                                       {
                                           ImagerUrl = resource.ImageUrl,
                                           ImageHeight = resource.ImageHeight,
                                           ImageWidth = resource.ImageWidth,
                                           ImageUrlSubdomains =
                                               new ObservableCollection<string>(resource.ImageUrlSubdomains),
                                           Orientation = resource.Orientation,
                                           TilesX = resource.TilesX,
                                           TilesY = resource.TilesY,
                                           Type = GetMetadataType(resource),
                                           ZoomMax = resource.ZoomMax,
                                           ZoomMin = resource.ZoomMin
                                       };

                    if (resource.VintageEnd != null)
                    {
                        metadata.VintageEnd = DateTime.Parse(resource.VintageEnd, CultureInfo.InvariantCulture);
                    }

                    if (resource.VintageStart != null)
                    {
                        metadata.VintageStart = DateTime.Parse(resource.VintageStart, CultureInfo.InvariantCulture);
                    }

                    imageryMetadataSearchResult.Imagery.Add(metadata);
                }
            }

            return imageryMetadataSearchResult;
        }
        
        private static IEnumerable<RouteWarning> MapWarnings(IEnumerable<Warning> warnings)
        {
            return warnings == null
                       ? Enumerable.Empty<RouteWarning>()
                       : warnings.Select(warning => new RouteWarning
                                                        {
                                                            Severity = warning.Severity,
                                                            Text = warning.Text,
                                                            WarningType = warning.WarningType
                                                        });
        }

        private static IEnumerable<RouteTransitLine> MapTransitLine(IEnumerable<TransitLine> transitLine)
        {
            return transitLine == null
                       ? Enumerable.Empty<RouteTransitLine>()
                       : transitLine.Select(line => new RouteTransitLine
                                                        {
                                                            AbbreviatedName = line.AbbreviatedName,
                                                            AgencyId = line.AgencyId,
                                                            AgencyName = line.AgencyName,
                                                            LineColor = line.LineColor,
                                                            LineTextColor = line.LineTextColor,
                                                            PhoneNumber = line.PhoneNumber,
                                                            ProviderInfo = line.ProviderInfo,
                                                            Uri = new Uri(line.Uri),
                                                            VerboseName = line.VerboseName
                                                        });
        }

        private static RouteInstruction MapInstruction(Instruction instruction)
        {
            return instruction == null
                       ? null
                       : new RouteInstruction
                             {
                                 ManeuverType = ConversionHelper.ParseEnum(instruction.ManeuverType, () => ManeuverType.Unknown),
                                 Text = instruction.Text
                             };
        }

        private static IEnumerable<RouteHint> MapHints(IEnumerable<Hint> hints)
        {
            return hints == null
                       ? Enumerable.Empty<RouteHint>()
                       : hints.Select(hint => new RouteHint
                                                  {
                                                      HintType = hint.HintType,
                                                      Text = hint.Text
                                                  });
        }

        private IEnumerable<ItineraryItem> MapItineraryItems(IEnumerable<Resources.Routes.ItineraryItem> itineraryItems)
        {
            return itineraryItems == null
                       ? Enumerable.Empty<ItineraryItem>()
                       : itineraryItems.Select(itineraryItem =>
                                                   {
                                                       var item = new ItineraryItem
                                                                      {
                                                                          CompassDirection = itineraryItem.CompassDirection,
                                                                          Exit = itineraryItem.Exit,
                                                                          IconType = itineraryItem.IconType,
                                                                          Instruction = MapInstruction(itineraryItem.Instruction),
                                                                          ManeuverPoint = MapPoint(itineraryItem.ManeuverPoint),
                                                                          SideOfStreet = itineraryItem.SideOfStreet,
                                                                          Time = itineraryItem.Time,
                                                                          TollZone = itineraryItem.TollZone,
                                                                          TowardsRoadName = itineraryItem.TowardsRoadName,
                                                                          TransitStopId = itineraryItem.TransitStopId,
                                                                          TransitTerminus = itineraryItem.TransitTerminus,
                                                                          TravelDistance = itineraryItem.TravelDistance,
                                                                          TravelDuration = itineraryItem.TravelDuration,
                                                                          TravelMode = itineraryItem.TravelMode,
                                                                      };

                                                       item.Details.AddRange(MapDetails(itineraryItem.Details));
                                                       item.ChildItineraryItems.AddRange(this.MapItineraryItems(itineraryItem.ChildItineraryItems));
                                                       item.Hints.AddRange(MapHints(itineraryItem.Hints));
                                                       item.Signs.AddRange(itineraryItem.Sign);
                                                       item.TransitLine.AddRange(MapTransitLine(itineraryItem.TransitLine));
                                                       item.Warnings.AddRange(MapWarnings(itineraryItem.Warnings));

                                                       return item;
                                                   });
        }

        private static IEnumerable<RouteDetail> MapDetails(IEnumerable<Detail> details)
        {
            return details == null
                       ? Enumerable.Empty<RouteDetail>()
                       : details.Select(detail =>
                                            {
                                                var routeDetail = new RouteDetail
                                                                      {
                                                                          CompassDegrees = detail.CompassDegrees,
                                                                          ManeuverType = detail.ManeuverType,
                                                                          Mode = detail.Mode,
                                                                          RoadType = detail.RoadType,
                                                                      };

                                                routeDetail.Names.AddRange(detail.Names);
                                                routeDetail.EndPathIndices.AddRange(detail.EndPathIndices);
                                                routeDetail.StartPathIndices.AddRange(detail.StartPathIndices);

                                                return routeDetail;
                                            });
        }

        private static Confidence MapConfidence(Resources.Confidence confidence)
        {
            switch (confidence)
            {
                case Resources.Confidence.High:
                    {
                        return Confidence.High;
                    }
                case Resources.Confidence.Medium:
                    {
                        return Confidence.Medium;
                    }
                case Resources.Confidence.Low:
                    {
                        return Confidence.Low;
                    }
                case Resources.Confidence.Unknown:
                    {
                        return Confidence.Unknown;
                    }
            }

            return Confidence.Unknown;
        }

        private static LocationRect MapBoundingBox(IList<double?> boundingBox)
        {
            if (boundingBox.Count != 4)
                return null;

            if ((boundingBox[0] != null) &&
                (boundingBox[1] != null) &&
                (boundingBox[2] != null) &&
                (boundingBox[3] != null))
                {
                            return new LocationRect(
                                boundingBox[0].GetValueOrDefault(),
                                boundingBox[1].GetValueOrDefault(),
                                boundingBox[2].GetValueOrDefault(),
                                boundingBox[3].GetValueOrDefault());
               }

            return null;
        }

        private static GeoCoordinate MapPoint(Point point)
        {
            if ((point.Coordinates[0] != null) && (point.Coordinates[1] != null))
            {
                return new GeoCoordinate(point.Coordinates[0].GetValueOrDefault(), point.Coordinates[1].GetValueOrDefault());
            }

            return GeoCoordinate.Unknown;
        }

        private string BuildQueryString(WayPoint waypoint, params string[] args)
        {
            var sb = new StringBuilder();

            if (waypoint.HasPoint)
            {
                sb.Append(string.Format(CultureInfo.InvariantCulture, "&waypoint.{0}={1},{2}", args[0], waypoint.Point.Latitude, waypoint.Point.Longitude));
                return sb.ToString();
            }

            if (waypoint.HasLandmark)
            {
                sb.Append(string.Format("&waypoint.{0}={1},{2}", args[0],
                                        propertyEncoder.Encode(waypoint.Landmark.AreaName),
                                        propertyEncoder.Encode(waypoint.Landmark.PostalCode)));
                return sb.ToString();
            }

            if (waypoint.HasAddress)
            {
                var multiple = false;

                sb.Append(string.Format("&waypoint.{0}=", args[0]));
                if (waypoint.Address.HasFormattedAddress)
                {
                    sb.Append(propertyEncoder.Encode(waypoint.Address.FormattedAddress));
                    return sb.ToString();
                }

                if (waypoint.Address.HasAddressLine)
                {
                    multiple = true;
                    sb.Append(propertyEncoder.Encode(waypoint.Address.AddressLine));
                }

                if (waypoint.Address.HasAdminDistrict)
                {
                    if (multiple)
                        sb.Append(Spacer);
                    else multiple = true;

                    sb.Append(propertyEncoder.Encode(waypoint.Address.AdminDistrict));
                }

                if (waypoint.Address.HasPostalCode)
                {
                    if (multiple)
                        sb.Append(Spacer);

                    sb.Append(propertyEncoder.Encode(waypoint.Address.PostalCode));
                }

                return sb.ToString();
            }

            return sb.ToString();
        }

        private string BuildQueryString(RouteDestination destination)
        {
            var sb = new StringBuilder();

            if (destination.HasPoint)
            {
                sb.Append(string.Format(CultureInfo.InvariantCulture, "{0},{1}", destination.Point.Latitude, destination.Point.Longitude));
                return sb.ToString();
            }

            if (destination.HasLandmark)
            {
                sb.Append(destination.Landmark.HasPostalCode
                              ? string.Format("{0},{1}", destination.Landmark.AreaName, destination.Landmark.PostalCode)
                              : string.Format("{0}", destination.Landmark.AreaName));

                return sb.ToString();
            }

            if (destination.HasAddress)
            {
                var multiple = false;

                if (destination.Address.HasFormattedAddress)
                {
                    sb.Append(destination.Address.FormattedAddress);
                    return sb.ToString();
                }

                if (destination.Address.HasAddressLine)
                {
                    multiple = true;
                    sb.Append(destination.Address.AddressLine);
                }

                if (destination.Address.HasAdminDistrict)
                {
                    if (multiple)
                        sb.Append(Spacer);
                    else multiple = true;

                    sb.Append(destination.Address.AdminDistrict);
                }

                if (destination.Address.HasPostalCode)
                {
                    if (multiple)
                        sb.Append(Spacer);

                    sb.Append(destination.Address.PostalCode);
                }

                return sb.ToString();
            }

            return sb.ToString();
        }

        private static string BuildQueryString(Avoid avoid)
        {
            var sb = new StringBuilder();

            var multiple = false;
            if (avoid.Highways)
            {
                multiple = true;
                sb.Append("highways");
            }
            if (avoid.Tolls)
            {
                if (multiple)
                    sb.Append(",");
                sb.Append("tolls");
            }
            if (avoid.MinimizeHighways)
            {
                if (multiple)
                    sb.Append(",");
                sb.Append("minimizeHighways");
            }
            if (avoid.MinimizeTolls)
            {
                if (multiple)
                    sb.Append(",");
                sb.Append("minimizeTolls");
            }

            return sb.ToString();
        }

        private static string BuildQueryString(IEnumerable<LocationEntity> includedEntities)
        {
            var sb = new StringBuilder();
            foreach (var includedEntity in includedEntities)
            {
                if (sb.Length != 0)
                    sb.Append(",");

                sb.Append(includedEntity);
            }

            return sb.ToString();
        }

        private static MetadataType GetMetadataType(Resources.ImageryMetadata.Metadata metadata)
        {
            var type = metadata.Type.Split(':')[0];

            if (type == "ImageryMetadata")
            {
                return MetadataType.Imagery;
            }

            if (type == "BirdseyeMetadata")
            {
                return MetadataType.Birdseye;
            }
            
            return MetadataType.Imagery;
        }
    }
}

